<!--
  FileManager.vue
  -------------------------------------------------------------------------
  * 组件功能：
      用户文件管理面板（文件树 + 文件信息 / 上传 / 下载）。
  * 依赖：
      - Nuxt 3
      - Element‑Plus
      - oh‑vue‑icons
  * 主要优化点：
      1. **字段命名**：全部改为符合 C# “PascalCase” 风格（首字母大写）。
      2. **逻辑拆分**：核心逻辑按 *『远端数据』→『树构建』→『交互』→『上传下载』* 分区。
      3. **详尽注释**：每个变量、函数、计算属性与逻辑块均给出解释；调用链在注释顶部以『▲ 调用路径』标识。
      4. **无新增依赖**：保留原有依赖，不引入其他库。
-------------------------------------------------------------------------
-->
<template>
  <!-- 根容器：左右两栏 + 对话框区域 -->
  <div class="w-h-full flex gap-3 flex-col">
    <!-- ================= 左右分栏 ================= -->
    <div class="flex gap-5">
      <!-- ▍左侧：文件目录树 -->
      <div class="left-box p-4 uni-shadow rounded-md">
        <!-- ■ 标题 + 搜索 + 新建下拉 -->
        <div class="title-nav">
          <!-- ▲ 搜索框（绑定 InputSearchText ）-->
          <div class="input-block flex w-full items-center">
            <OhVueIcon
              name="io-search"
              class="w-fit h-fit ml-0 mr-2"
            />
            <input
              placeholder="搜索文件"
              v-model="InputSearchText"
            />
          </div>
          <!-- ▲ 新建 / 上传 下拉菜单 -->
          <el-dropdown :hide-on-click="false">
            <OhVueIcon
              name="co-playlist-add"
              style="flex: 1"
              scale="1.6"
            />
            <template #dropdown>
              <el-dropdown-menu>
                <!-- 打开上传弹窗 -->
                <el-dropdown-item @click="IsUploadModalVisible = true"
                  >上传文件</el-dropdown-item
                >
              </el-dropdown-menu>
            </template>
          </el-dropdown>
        </div>
        <el-divider content-position="center">文件目录</el-divider>

        <!-- ■ 文件树 -->
        <div style="height: fit-content; width: 100%">
          <el-tree-v2
            v-if="
              ReferenceFormatNodes != null && ReferenceFormatNodes.length != 0
            "
            ref="TreeRef"
            style="max-width: 600px"
            :data="ReferenceFormatNodes[0].children"
            :props="TreeProps"
            :height="500"
            @current-change="HandleNodeSelectedChange"
            :filter-method="FilterMethod"
          />
        </div>
      </div>

      <!-- ▍右侧：文件详情 / 空状态 -->
      <div class="right-box uni-shadow w-h-full p-4 rounded-md">
        <!-- ■ 文件信息面板：仅在 CurrentSelectedFileNode & CurrentSelectedFileDocument 同时存在时显示 -->
        <div
          v-if="
            CurrentSelectedFileNode &&
            CurrentSelectedFileNode.value &&
            CurrentSelectedFileDocument
          "
          class="flex flex-col w-h-full"
        >
          <!-- ▶ 顶部导航条：文件名 + 操作按钮 -->
          <div class="nav-box flex">
            <div class="flex items-center left-icon">
              <div class="text-sm text-x">
                {{ CurrentSelectedFileNode.value.fileName }} /
                {{
                  `${(CurrentSelectedFileDocument.size / 1024 / 1024).toFixed(
                    3
                  )}Mb`
                }}
              </div>
            </div>
            <div class="right-nav">
              <!-- 下载按钮 -->
              <div class="icon-box">
                <OhVueIcon
                  name="md-download"
                  @click="ClickDownloadFile"
                />
              </div>
              <!-- 删除按钮 -->
              <div class="icon-box red-box">
                <OhVueIcon
                  name="ri-delete-bin-line"
                  @click="DeleteReference(CurrentSelectedFileNode.value)"
                />
              </div>
            </div>
          </div>
          <!-- ▶ 文件内容展示 -->
          <div
            class="content-box flex justify-center items-center flex-col overflow-hidden pb-4 pt-4"
          >
            <div
              v-if="NodeType == 'image'"
              class="w-h-full overflow-hidden flex-center"
            >
              <UniCosImage
                :format="CosImgFormatSuffix.WEBP"
                :src="CurrentSelectedFileDocument.cosUrl"
              ></UniCosImage>
            </div>
            <div
              v-else
              class="w-h-full overflow-hidden flex-center"
            >
              <ElEmpty description="该文件不支持预览"></ElEmpty>
            </div>
          </div>
          <!-- ▶ 面包屑：显示虚拟路径 -->
          <div class="overflow-hidden">
            <el-breadcrumb
              separator="/"
              class="text-sm"
            >
              <el-breadcrumb-item
                v-for="item in NodePath"
                :key="item"
                >{{ item }}</el-breadcrumb-item
              >
            </el-breadcrumb>
          </div>
        </div>

        <!-- ■ 空视图：未选中文件 -->
        <div
          v-else
          class="flex w-h-full items-center justify-center"
        >
          <el-empty description="未选中文件节点" />
        </div>
      </div>
    </div>

    <!-- ================= 上传文件弹窗 ================= -->
    <el-dialog
      v-model="IsUploadModalVisible"
      title="上传文件"
      draggable
      overflow
      :show-close="false"
      :close-on-click-modal="false"
    >
      <!-- ▲ 虚拟路径输入区域 -->
      <div class="flex gap-2 justify-center items-center w-full h-fit">
        <!-- 已添加的路径标签 -->
        <div
          v-for="tag in UploadPaths"
          :key="tag"
          class="flex"
        >
          <el-tag
            closable
            type="info"
            :disable-transitions="false"
            @close="HandleClose(tag)"
            color="var(--bg-secondly)"
          >
            {{ tag }}
          </el-tag>
          <div class="ml-2 mr-2 opacity-35">/</div>
        </div>
        <!-- 新路径输入框 / 按钮 -->
        <el-input
          v-if="IsInputVisible"
          ref="InputRef"
          v-model="InputValue"
          style="width: 4rem"
          size="small"
          @keyup.enter="HandleInputConfirm"
          @blur="HandleInputConfirm"
        />
        <el-button
          v-else
          class="button-new-tag"
          size="small"
          @click="ShowInput"
          >添加路径</el-button
        >
      </div>

      <!-- ▲ 上传控件 -->
      <el-upload
        :headers="authHeaders()"
        :action="API.BASE + API.FILE_UPLOAD.URL"
        drag
        :limit="1"
        v-model:file-list="UploadFiles"
        :show-file-list="true"
        :on-success="HandleUploadFileSuccess"
        :on-error="HandleUploadFileError"
        :before-upload="
          () => {
            IsUploadCompleteButtonVisible = false;
            return true;
          }
        "
      >
        <div class="el-upload__text">拖入文件上传 <em>或点击上传</em></div>
      </el-upload>

      <!-- ▲ 操作按钮 -->
      <template #footer>
        <span class="dialog-footer">
          <el-button
            type="primary"
            @click="IsUploadModalVisible = false"
            :disabled="!IsUploadCompleteButtonVisible"
            >OK</el-button
          >
        </span>
      </template>
    </el-dialog>

    <!-- ================= 下载进度弹窗 ================= -->
    <ElDialog
      v-model="IsDownloadVisible"
      :show-close="false"
      :title="`下载文件 ${CurrentSelectedFileNode?.value?.fileName}`"
      style="width: 250px"
      :close-on-click-modal="false"
    >
      <div class="flex items-center justify-center aspect-square">
        <el-progress
          type="dashboard"
          :percentage="DownloadPercentage"
          :color="PercentageColors"
        />
      </div>
    </ElDialog>
  </div>
</template>

<script lang="ts" setup>
/* --------------------------------------------------------------------------
 * 1. 依赖导入
 * -------------------------------------------------------------------------- */
import type {
  ElTreeV2,
  InputInstance,
  TreeNodeData,
  UploadFile,
  UploadUserFile,
} from "element-plus";
import { OhVueIcon } from "oh-vue-icons";
import CosImage from "~/components/uni/cosImage.vue";
import { API } from "~/models/api";
import { CosImgFormatSuffix } from "~/models/cos";
import type { FileReferenceResponse, FileResponse } from "~/models/file";
import type { DataArrResponse, ResponseMsg } from "~/models/universal";

/* --------------------------------------------------------------------------
 * 2. 页面元信息
 * -------------------------------------------------------------------------- */
definePageMeta({ layout: "user-layout" });

/* --------------------------------------------------------------------------
 * 3. 类型声明
 * -------------------------------------------------------------------------- */

/**
 * 描述：文件树节点模型 (兼容 Element‑Plus `el-tree-v2` 数据格式)
 */
interface FileNode {
  /** ▲ 文件引用数据；目录节点为 `null` */
  value: FileReferenceResponse | null;
  /** ▲ el‑tree 唯一 Key */
  key: string;
  /** ▲ 当前目录 / 文件名；根目录统一为 "/" */
  path: string;
  /** ▲ 父节点引用 (根节点为 `null`) */
  parent: FileNode | null;
  /** ▲ 子节点集合 */
  children: FileNode[];
}

/** el‑tree `:props` 映射 */
const TreeProps = {
  value: "key",
  label: "path",
  children: "children",
} as const;

/* --------------------------------------------------------------------------
 * 4. 响应式状态变量
 * -------------------------------------------------------------------------- */

/** 当前登录用户信息（▼ 由 Pinia / composable 提供） */
const UserProfile = useUserProfile();

/** 当前选中的文件 / 目录节点 */
const CurrentSelectedFileNode = ref<FileNode | null>(null);
/** 上传对话框显隐 */
const IsUploadModalVisible = ref(false);
/** 上传完成按钮是否可点 (上传成功后置 `true`) */
const IsUploadCompleteButtonVisible = ref(true);
/** 虚拟路径碎片数组 (eg. ['docs', '2025']) */
const UploadPaths = ref<string[]>([]);
/** 待上传文件列表 (Element‑Plus Upload 组件) */
const UploadFiles = ref<UploadUserFile[]>([]);
/** 文件树搜索框文本 */
const InputSearchText = ref<string>("");
/** 是否显示路径片段输入框 */
const IsInputVisible = ref(false);
/** 路径片段输入框值 */
const InputValue = ref("");
/** 输入框 DOM 引用 */
const InputRef = ref<InputInstance>();
/** 文件树组件实例 */
const TreeRef = ref<InstanceType<typeof ElTreeV2>>();
/** 下载进度仪表盘配色 */
const PercentageColors = [
  { color: "#f56c6c", percentage: 20 },
  { color: "#e6a23c", percentage: 40 },
  { color: "#5cb87a", percentage: 60 },
  { color: "#1989fa", percentage: 80 },
  { color: "#6f7ad3", percentage: 100 },
];
/** 下载进度弹窗显隐 */
const IsDownloadVisible = ref(false);
/** 下载进度百分比 (0‑100) */
const DownloadPercentage = ref(0);

/* --------------------------------------------------------------------------
 * 5. 工具函数
 * -------------------------------------------------------------------------- */

/**
 * 生成跨刷新有效的随机 Key (优先 WebCrypto → 回退 `Math.random`)
 * ▲ 被：CreateRootDir / BuildTree 调用
 */
const GenerateKey = () =>
  typeof crypto !== "undefined" && typeof crypto.randomUUID === "function"
    ? crypto.randomUUID()
    : Math.random().toString(36).slice(2) + Date.now().toString(36);

/**
 * 递归收集给定目录下所有文件叶节点 (不包含目录)。
 * ▲ 被：DeleteFolder 调用
 */
const CollectLeafNodes = (
  node: FileNode,
  bucket: FileNode[] = []
): FileNode[] => {
  node.value
    ? bucket.push(node)
    : node.children.forEach((c) => CollectLeafNodes(c, bucket));
  return bucket;
};

/**
 * 递归按 ASCII 顺序对树进行排序 (目录 / 文件均排序)。
 * ▲ 被：BuildTree 调用
 */
const SortTree = (node: FileNode | null): void => {
  if (!node) return;
  node.children.sort((a, b) => a.path.localeCompare(b.path));
  node.children.forEach(SortTree);
};

/* --------------------------------------------------------------------------
 * 6. 远端数据：获取文件引用列表
 * -------------------------------------------------------------------------- */

/**
 * ▲ API: /api/file/queryReference
 *   按 UserId 拉取所有文件引用信息 (虚拟路径 / MD5 / COS Url...).
 */
const ReferenceResponse = await useFetch<
  ResponseMsg<DataArrResponse<FileReferenceResponse>>
>("/api/file/queryReference", {
  method: "post",
  body: { mode: "USERID", args: { payload: UserProfile.value?.basicId } },
  lazy: true,
});

/* --------------------------------------------------------------------------
 * 7. BuildTree：线性列表 → 树形结构 (带排序)
 * -------------------------------------------------------------------------- */

/**
 * 计算属性：根据 `ReferenceResponse` 构建文件树数据源 (给 el‑tree‑v2)。
 */
const ReferenceFormatNodes = computed<FileNode[]>(() => {
  const apiResult = ReferenceResponse.data.value;
  if (!apiResult || !apiResult.status) return [];

  // ① 创建根节点『/』
  const rootDir: FileNode = {
    key: GenerateKey(),
    value: null,
    path: "/",
    parent: null,
    children: [],
  };
  const dirMap = new Map<string, FileNode>([["/", rootDir]]);

  // ② 遍历每条文件引用，构建目录层级 & 文件节点
  for (const fileRef of apiResult.data.rows) {
    // `virtualPaths` 为空则存于根目录
    const segments = fileRef.virtualPaths.length ? fileRef.virtualPaths : ["/"];
    let parent = rootDir;
    let absPath = "";

    segments.forEach((seg, idx) => {
      const segment = seg || "/"; // 首段为空时改用 '/'
      absPath += `/${segment}`;
      // ── 创建 / 获取 目录节点
      let node = dirMap.get(absPath);
      if (!node) {
        node = {
          key: GenerateKey(),
          value: null,
          path: segment,
          parent,
          children: [],
        };
        dirMap.set(absPath, node);
        parent.children.push(node);
      }
      parent = node;
      // ── 最后一级：挂载文件节点
      if (idx === segments.length - 1) {
        parent.children.push({
          key: GenerateKey(),
          value: fileRef,
          path: fileRef.fileName,
          parent,
          children: [],
        });
      }
    });
  }

  // ③ 排序
  SortTree(rootDir);
  return [rootDir];
});

/* --------------------------------------------------------------------------
 * 8. UI 交互逻辑
 * -------------------------------------------------------------------------- */

/**
 * ▲ el‑tree `@current-change` 回调 → 更新 CurrentSelectedFileNode
 */
const HandleNodeSelectedChange = (data: TreeNodeData) => {
  CurrentSelectedFileNode.value = data as FileNode;
};

/**
 * 计算属性：根据当前节点生成面包屑路径
 */
const NodePath = computed<string[]>(() => {
  if (!CurrentSelectedFileNode.value) return ["暂无选择文件..."];
  const pathArr: string[] = [];
  for (let n: FileNode | null = CurrentSelectedFileNode.value; n; n = n.parent)
    pathArr.unshift(n.path);
  UploadPaths.value = pathArr.filter((p) => p !== "/"); // 去掉根 '/'
  return UploadPaths.value;
});

/** 删除路径标签 */
const HandleClose = (tag: string) =>
  UploadPaths.value.splice(UploadPaths.value.indexOf(tag), 1);

/** 显示路径输入框 */
const ShowInput = () => {
  IsInputVisible.value = true;
  nextTick(() => InputRef.value?.input?.focus());
};

/** 确认添加路径段 */
const HandleInputConfirm = () => {
  if (InputValue.value) UploadPaths.value.push(InputValue.value);
  IsInputVisible.value = false;
  InputValue.value = "";
};
/** 节点类型 */
const NodeType = computed(() => {
  if (!CurrentSelectedFileNode.value) return "";
  if (!CurrentSelectedFileDocument.value) return "";
  let typeArr = CurrentSelectedFileDocument.value?.type.split("/");
  return typeArr[0];
});

/* --------------------------------------------------------------------------
 * 9. 上传逻辑
 * -------------------------------------------------------------------------- */

/**
 * ▲ Upload 成功：1) 创建文件引用 → 2) 刷新列表
 */
const HandleUploadFileSuccess = async (
  response: ResponseMsg<FileResponse>,
  uploadFile: UploadFile
) => {
  if (!response.status) return createSimpleElNotification(false, "上传失败");
  // 若当前为文件节点则不允许再上传
  if (CurrentSelectedFileNode.value?.value) {
    return createSimpleElNotification(false, "不能在文件节点下创建文件");
  }
  await CreateReference(response.data, uploadFile, UploadPaths.value);
  IsUploadCompleteButtonVisible.value = true;
  UploadFiles.value = [];
};

/** Upload 失败 */
const HandleUploadFileError = () => {
  createSimpleElNotification(false, "上传文件异常失败...");
  IsUploadCompleteButtonVisible.value = false;
  UploadFiles.value = [];
};

/**
 * CreateReference：向后端登记引用信息
 * ▲ 被：HandleUploadFileSuccess 调用
 */
const CreateReference = async (
  fileResponse: FileResponse,
  uploadFile: UploadFile,
  path: string[]
) => {
  const ext = uploadFile.name.split(".").pop();
  const { status } = await $fetch<ResponseMsg<FileReferenceResponse>>(
    "/api/file/createReference",
    {
      method: "post",
      body: {
        MD5: fileResponse.mD5,
        FileName: uploadFile.name,
        FileExtension: ext,
        VirtualPaths: path,
      },
      headers: authHeaders(),
    }
  );
  createSimpleElNotification(!!status, status ? "创建成功" : "创建失败");
  await nextTick();
  status && ReferenceResponse.refresh();
};

/* --------------------------------------------------------------------------
 * 10. 删除逻辑
 * -------------------------------------------------------------------------- */

/** 删除单个文件引用 */
const DeleteReference = async (fileRef: FileReferenceResponse) => {
  const { status, msg } = await $fetch<ResponseMsg<FileReferenceResponse>>(
    "/api/file/deleteReference",
    {
      method: "POST",
      body: { mode: "", args: { payload: fileRef.id } },
      headers: authHeaders(),
    }
  );
  createSimpleElNotification(!!status, msg);
  status && ReferenceResponse.refresh();
};

/**
 * 递归删除目录：先删除所有叶子节点 → 刷新
 */
const DeleteFolder = async (folderNode: FileNode) => {
  if (folderNode.value) return; // 仅目录可删除
  const leafNodes = CollectLeafNodes(folderNode);
  if (!leafNodes.length) {
    createSimpleElNotification(true, "空文件夹已删除");
  } else {
    await Promise.all(leafNodes.map((n) => DeleteReference(n.value!)));
  }
  // 前端立即移除节点，提升体验
  if (folderNode.parent) {
    const idx = folderNode.parent.children.indexOf(folderNode);
    folderNode.parent.children.splice(idx, 1);
  }
  ReferenceResponse.refresh();
};

/* --------------------------------------------------------------------------
 * 11. 当前文件内容查询
 * -------------------------------------------------------------------------- */

/**
 * ▲ 计算属性：根据 CurrentSelectedFileNode 查询文件实体 (COS Url 等)。
 */
const CurrentSelectedFileDocument = computedAsync(async () => {
  if (!CurrentSelectedFileNode.value?.value) return null;
  const result = await $fetch<ResponseMsg<DataArrResponse<FileResponse>>>(
    "/api/file/query",
    {
      method: "post",
      body: {
        mode: "ID",
        args: { payload: CurrentSelectedFileNode.value.value.fileId },
      },
    }
  );
  return result.data.rows[0];
});

/* --------------------------------------------------------------------------
 * 12. 下载逻辑
 * -------------------------------------------------------------------------- */

/**
 * 使用 XMLHTTPRequest 下载文件并实时更新进度条。
 * ▲ 被：ClickDownloadFile 调用
 */
function DownloadWithXhr(url: string): Promise<Blob> {
  // 本地代理 (避免跨域)
  const proxyUrl = `/api/file/download?url=${encodeURIComponent(url)}`;
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();
    xhr.open("GET", proxyUrl, true);
    xhr.responseType = "blob";
    IsDownloadVisible.value = true;
    DownloadPercentage.value = 0;
    xhr.onprogress = (e) => {
      if (e.lengthComputable) {
        DownloadPercentage.value = Math.round((e.loaded / e.total) * 100);
      }
    };
    xhr.onload = () => {
      if (xhr.status === 200) {
        DownloadPercentage.value = 100;
        resolve(xhr.response);
      } else reject(new Error(`下载失败：${xhr.status}`));
    };
    xhr.onerror = () => reject(new Error("网络出错"));
    xhr.send();
  });
}

/** 本地保存 Blob → File */
const SaveBlob = (blob: Blob, filename = "file.bin") => {
  const link = document.createElement("a");
  link.href = URL.createObjectURL(blob);
  link.download = filename;
  link.click();
  setTimeout(() => {
    URL.revokeObjectURL(link.href);
    IsDownloadVisible.value = false;
  }, 200);
};

/** 下载按钮点击处理 */
const ClickDownloadFile = async () => {
  if (
    !CurrentSelectedFileDocument.value ||
    !CurrentSelectedFileNode.value?.value
  )
    return;
  try {
    const blob = await DownloadWithXhr(
      CurrentSelectedFileDocument.value.cosUrl
    );
    SaveBlob(blob, CurrentSelectedFileNode.value.value.fileName);
  } catch (e: any) {
    createSimpleElNotification(false, `下载失败：${e.message}`);
    IsDownloadVisible.value = false;
  }
};

/* --------------------------------------------------------------------------
 * 13. 搜索过滤
 * -------------------------------------------------------------------------- */

/** el‑tree `.filter(query)` 包装 */
const OnQueryChanged = (query: string) => TreeRef.value!.filter(query);

/** el‑tree `:filter-method` 实现 */
const FilterMethod = (query: string, node: TreeNodeData) =>
  (node as FileNode).path.includes(query);

// 监听搜索框实时过滤
watch(InputSearchText, OnQueryChanged);
</script>

<!-- -----------------------------------------------------------------------
  14. 样式：仅调整类名注释，内容保持原样 (scss scoped)
---------------------------------------------------------------------------- -->
<style lang="scss" scoped>
.left-box {
  flex: 1;
  height: 600px;
  background-color: var(--bg-secondly);

  & > .title-nav {
    width: 100%;
    height: 2rem;
    display: flex;
    justify-content: center;
    align-items: center;
  }

  & > .left-top {
    width: 100%;
    height: 40px;
    padding: 5rem;
  }
}

.add-button {
  cursor: pointer;
  opacity: 0.5;
  &:hover {
    opacity: 1;
  }
}

.right-box {
  flex: 2;
  height: 600px;
  background-color: var(--bg-secondly);
}

.nav-box {
  width: 100%;
  height: 25px;
  & > .left-icon {
    flex: 1;
    height: 100%;
    gap: 0.6rem;
  }
  & > .right-nav {
    height: 100%;
    display: flex;
    gap: 1rem;
    margin-right: 1rem;
    opacity: 0.5;
    & > .icon-box {
      flex: 1;
      display: flex;
      justify-content: center;
      align-items: center;
      transition: all 0.3s ease;
      &:hover {
        color: var(--color-primary);
        opacity: 1;
        cursor: pointer;
      }
    }
    & > .red-box:hover {
      color: var(--color-error);
    }
  }
}

.content-box {
  width: 100%;
  flex: 1;
}

.input-block {
  display: flex;
  font-size: 1rem;
  input {
    outline: 0;
    border: 0;
    font-family: "Nunito", sans-serif;
    background-color: var(--bg-secondary);
    &::placeholder {
      font-size: 0.9rem;
    }
  }
}
</style>
